// Default map zoom.
var DEFAULT_ZOOM = 1;
// Map zoom for search results.
var FIND_ZOOM = 15;
// Default map center.
var DEFAULT_CENTER = new google.maps.LatLng(0, 0);
// Default StreetView zoom.
var DEFAULT_SV_ZOOM = 1;
// Default camera distance in StreetView.
var STREET_VIEW_DISTANCE = 50;

// Poll refresh rate for planet request.
var POLL_TIME = 1000;


// Manages client-side interaction.
function StreetViewPlanets(mapOptions, panoramaOptions) {
  // Create a map.
  var mapDiv = document.getElementById('mapCanvas');
  this.map = new google.maps.Map(mapDiv, mapOptions);

  // Create a panorama.
  var panoramaDiv = document.getElementById('panoCanvas');
  this.panorama = new google.maps.StreetViewPanorama(panoramaDiv,
      panoramaOptions);
  // Associate the map with the separate panorama.
  this.map.setStreetView(this.panorama);

  // Get Geocoder and StreetViewService objects.
  this.geocoder = new google.maps.Geocoder();
  this.streetview = new google.maps.StreetViewService();

  // Current location marker.
  this.marker = null;
  // Geocoding result.
  this.geoResult = null;
  // Street View result.
  this.panoramaData = null;

  // Set submit handler for address search.
  $('#addressForm').submit(bind(this, this.addressFormSubmit));
  // Set submit handler for planet request.
  $('#planetForm').submit(bind(this, this.planetFormSubmit));
  // Set handler for Street View panorama visibility changing.
  google.maps.event.addListener(this.panorama, 'visible_changed',
      bind(this, this.panoramaVisibleChanged));
}

// Called when the search form is submitted.
StreetViewPlanets.prototype.addressFormSubmit = function(event) {
  // Do not submit form.
  event.preventDefault();

  // Clear the previous marker.
  if (this.marker != null) {
    this.marker.setMap(null);
    this.marker = null;
  }

  // Clear old data.
  this.geoResult = null;
  this.panoramaData = null;

  // Hide the error bar.
  this.hideAddressError();
  // Exit current Street View preview.
  this.panorama.setVisible(false);

  // Fire a request to the Google Geocoder.
  var address = document.getElementById('addressText').value;
  this.geocoder.geocode({'address': address}, bind(this, this.geocodeRespond));
}

// Called when the Geocoder returns a result.
StreetViewPlanets.prototype.geocodeRespond = function(results, status) {
  // Take first result (we're always feeling lucky).
  this.geoResult = results[0];

  if (status == google.maps.GeocoderStatus.OK) {
    // Geocoding was successful.
    var location = this.geoResult.geometry.location;

    // Focus map on the result.
    this.map.setCenter(location);
    this.map.setZoom(FIND_ZOOM);

    // Place a marker at the resulting location.
    this.marker = new google.maps.Marker({
          map: this.map, 
          position: location,
          title: this.geoResult.formatted_address,
        });

    // Look for the nearest Street View panorama.
    this.streetview.getPanoramaByLocation(location, STREET_VIEW_DISTANCE,
        bind(this, this.streetViewRespond));
  } else {
    // Geocoding failed, display error.
    this.setAddressError('Could not find that location');
    this.showAddressError();
  }
}

// Called when the Street View service returns a result.
StreetViewPlanets.prototype.streetViewRespond = function(data, status) {
  if (status == google.maps.StreetViewStatus.OK) {
    // Street View search was successful.
    // Save response.
    this.panoramaData = data;
    // Set position and orientation of Street View camera.
    this.jumpToResult();

    // Add an event handler to the marker.
    google.maps.event.addListener(this.marker, 'click',
        bind(this, this.markerClick));
  } else {
    // Street View search was unsuccessful, display error message.
    this.setAddressError('Unable to find Street View near that location');
    this.showAddressError();
  }
}

// Called when the form to create a planet is submitted.
StreetViewPlanets.prototype.planetFormSubmit = function(event) {
  // Do not submit form.
  event.preventDefault();

  // Fade out previous image if it still exists.
  this.hidePlanet();
  // Display loading message.
  this.setLoadingMessage('Waiting for response from server');
  this.showLoadingMessage();

  // Create a request for a planet and monitor its status.
  var jobMonitor = new JobMonitor(this.panorama.getPano());
  jobMonitor.setCreate(bind(this, this.jobCreate));
  jobMonitor.setComplete(bind(this, this.jobComplete));
  jobMonitor.setLoad(bind(this, this.planetLoad));
}

StreetViewPlanets.prototype.jobCreate = function() {
  this.setLoadingMessage('Waiting in queue');
}

StreetViewPlanets.prototype.jobComplete = function() {
  this.setLoadingMessage('Receiving planet');
}

StreetViewPlanets.prototype.planetLoad = function(image) {
  // Hide the loading message.
  this.hideLoadingMessage();

  // Set the image and show it.
  document.getElementById('planetImage').src = image.src;
  this.showPlanet();
}

StreetViewPlanets.prototype.setLoadingMessage = function(text) {
  $('#loadingText').html(text);
}

StreetViewPlanets.prototype.hideLoadingMessage = function() {
  $('#loadingDiv').slideUp();
}

StreetViewPlanets.prototype.showLoadingMessage = function() {
  $('#loadingDiv').slideDown();
}

StreetViewPlanets.prototype.hidePlanet = function() {
  $('#planetImage').fadeOut('slow');
}

StreetViewPlanets.prototype.showPlanet = function() {
  $('#planetImage').fadeIn('slow');
}

StreetViewPlanets.prototype.jumpToResult = function() {
  if (this.panoramaData != null) {
    this.panorama.setPano(this.panoramaData.location.pano);
    var heading = headingBetween(
      this.panoramaData.location.latLng,
      this.geoResult.geometry.location);
    this.panorama.setPov({heading: heading, pitch: 0, zoom: DEFAULT_SV_ZOOM});
    this.panorama.setVisible(true);
  }
}

StreetViewPlanets.prototype.showAddressError = function() {
  $('#addressErrorDiv').show('fast');
}

StreetViewPlanets.prototype.hideAddressError = function() {
  $('#addressErrorDiv').hide('fast');
}

StreetViewPlanets.prototype.setAddressError = function(message) {
  $('#addressError').html(message);
}

StreetViewPlanets.prototype.panoramaVisibleChanged = function() {
  // Disable the planet button if map is not in Street View mode.
  var disable = !this.panorama.getVisible();
  if (disable) {
    $('#planetButton').button("disable");
  } else {
    $('#planetButton').button("enable");
  }
}

function main() {
  // Put watermark on search box.
  $('#addressText').Watermark('Enter an address or landmark');
  // Stylize buttons.
  $("#findButton").button();
  $("#planetButton").button();

  var mapOptions = {
    zoom: DEFAULT_ZOOM,
    center: DEFAULT_CENTER,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
    navigationControl: true,
    mapTypeControl: true,
    streetViewControl: true,
    backgroundColor: '#E5E3DF',
  };

  var panoramaOptions = {
    position: null,
    visible: true,
    addressControl: false,
    enableCloseButton: true,
    navigationControlOptions: {
      style: google.maps.NavigationControlStyle.ZOOM_PAN,
    },
  };

  var svp = new StreetViewPlanets(mapOptions, panoramaOptions);
}

function headingBetween(from, to) {
  // Get latitude and longitude of subject and camera in radians.
  var lat1 = from.lat() * Math.PI / 180;
  var lng1 = from.lng() * Math.PI / 180;
  var lat2 = to.lat() * Math.PI / 180;
  var lng2 = to.lng() * Math.PI / 180;

  // Linear(ish) vector between points.
  var dx = Math.cos(lat2) * (lng2 - lng1);
  var dy = lat2 - lat1;
  var theta = Math.atan2(dx, dy);

  // Convert to degrees.
  var heading = Math.round(theta * 180 / Math.PI);

  return heading;
}


function JobMonitor(panoId) {
  // Set the panorama ID.
  this.panoId = panoId;
  this.jobId = 0;

  // Ask the server to create a planet.
  var url = '/create/' + this.panoId;
  $.ajax({url: url, dataType: 'json', success: bind(this, this.create)});
}

JobMonitor.prototype.setCreate = function(createHandler) {
  this.createHandler = createHandler;
}

JobMonitor.prototype.setComplete = function(completeHandler) {
  this.completeHandler = completeHandler;
}

JobMonitor.prototype.setLoad = function(loadHandler) {
  this.loadHandler = loadHandler;
}

JobMonitor.prototype.create = function(data, status, request) {
  if (data.status == 'waiting') {
    // Call the handler for creating a request.
    if (this.createHandler) {
      this.createHandler();
    }

    // Panorama has been added to the queue.
    this.jobId = data.id;

    // Set up periodic poll.
    setTimeout(bind(this, this.poll), POLL_TIME);
  } else if (data.status == 'complete') {
    // That panorama already exists!
    this.jobId = data.id;
    this.loadImage('/planet/' + this.jobId + '.jpg');
  }
}

// Polls the server for the status of the job.
JobMonitor.prototype.poll = function() {
  // Request status of job.
  var url = '/status/' + this.jobId;
  $.ajax({url: url, dataType: 'json', success: bind(this, this.pollSuccess)});
}

// Handles a successful status poll.
JobMonitor.prototype.pollSuccess = function(data, status, request) {
  if (data.status == 'waiting') {
    // Still waiting. Set up another poll.
    setTimeout(bind(this, this.poll), POLL_TIME);
  } else if (data.status == 'complete') {
    // Image is completed!
    if (this.completeHandler) {
      this.completeHandler();
    }

    this.loadImage('/planet/' + this.jobId + '.jpg');
  } else if (data.status == 'error') {
    // An error occurred.
  }
}

JobMonitor.prototype.loadImage = function(url) {
  // Planet finished warping! Get image.
  var image = new Image();
  image.src = url;

  // When image loads, do something.
  if (this.loadHandler) {
    // Call load handler when the image loads.
    var callback = function() { this.loadHandler(image); };
    $(image).load(bind(this, callback));
  }
}

// Run initialize function once DOM is ready.
$(main);

